<?php

require_once 'intl/classes//timestamp.php';

class Intl_Locale
{
	CONST CACHE_PREFIX = 'intl.locale';
	/**
	 * @var string
	 */
	protected $_locale;

	/**
	 * @var array
	 */
	protected $_localeData = null;

	/**
	 * @var array
	 */
	protected $_numberFormats = array();

	/**
	 * pattern characters mapping to the corresponding translator methods to ISO format
	 * @var array
	 */
	static protected $_dateFormats = array(
		'G' => 'formatEra',
		'y' => 'formatYear',
		'M' => 'formatMonth',
		'L' => 'formatMonth',
		'd' => 'formatDay',
		'h' => 'formatHour12',
		'H' => 'formatHour24',
		'm' => 'formatMinutes',
		's' => 'formatSeconds',
		'E' => 'formatDayInWeek',
		'c' => 'formatDayInWeek',
		'e' => 'formatDayInWeek',
		'D' => 'formatDayInYear',
		'F' => 'formatDayInMonth',
		'w' => 'formatWeekInYear',
		'W' => 'formatWeekInMonth',
		'a' => 'formatPeriod',
		'k' => 'formatHourInDay',
		'K' => 'formatHourInPeriod',
		'z' => 'formatTimeZone',
		'Z' => 'formatTimeZone',
		'v' => 'formatTimeZone',
	);

	/**
	 * cast to string
	 * @return sttring
	 */
	public function __toString()
	{
		return 'locale {' . $this -> _locale . '}';
	}

	/**
	 * Constructor.
	 * Since the constructor is protected, please use {@link getInstance}
	 * to obtain an instance of the specified locale.
	 * @param string $id the locale ID (e.g. en_US)
	 */
	public function __construct($locale)
	{
		$this -> _locale = $locale;

		$dataFile = LEGACY_DIR . 'ldml/' . $this -> _locale . '.php';
	
		if (is_file($dataFile))
		{
			$this -> _localeData =	require ($dataFile);
		}
		else
		{
			throw new Exception(strtr('Unrecognized locale "{locale}"', array('{locale}' => $locale)));
		}
	}

	/**
	 * @param string $currency 3-letter ISO 4217 code. For example, the code "USD"
	 * represents the US Dollar and "EUR" represents the Euro currency.
	 * @return string the localized currency symbol. Null if the symbol does not
	 * exist.
	 */
	public function getCurrencySymbol($currency)
	{
		return isset($this -> _localeData['currencySymbols'][$currency]) ? $this -> _localeData['currencySymbols'][$currency] : null;
	}

	/**
	 * @param string $name symbol name
	 * @return string symbol
	 */
	public function getNumberSymbol($name)
	{
		return isset($this -> _localeData['numberSymbols'][$name]) ? $this -> _localeData['numberSymbols'][$name] : null;
	}

	/**
	 * @return string the decimal format
	 */
	public function getDecimalFormat()
	{
		return $this -> _localeData['decimalFormat'];
	}

	/**
	 * @return string the currency format
	 */
	public function getCurrencyFormat()
	{
		return $this -> _localeData['currencyFormat'];
	}

	/**
	 * @return string the percent format
	 */
	public function getPercentFormat()
	{
		return $this -> _localeData['percentFormat'];
	}

	/**
	 * @return string the scientific format
	 */
	public function getScientificFormat()
	{
		return $this -> _localeData['scientificFormat'];
	}

	/**
	 * @param integer $month month (1-12)
	 * @param string $width month name width. It can be 'wide', 'abbreviated' or
	 * 'narrow'.
	 * @param boolean $standAlone whether the month name should be returned in
	 * stand-alone format
	 * @return string the month name
	 */
	public function getMonthName($month, $width = 'wide', $standAlone = false)
	{
		if ($standAlone)
		{
			return isset($this -> _localeData['monthNamesSA'][$width][$month]) ? $this -> _localeData['monthNamesSA'][$width][$month] : $this -> _localeData['monthNames'][$width][$month];
		}
		return isset($this -> _localeData['monthNames'][$width][$month]) ? $this -> _localeData['monthNames'][$width][$month] : $this -> _localeData['monthNamesSA'][$width][$month];
	}

	/**
	 * Returns the month names in the specified width.
	 * @param string $width month name width. It can be 'wide', 'abbreviated' or
	 * 'narrow'.
	 * @param boolean $standAlone whether the month names should be returned in
	 * stand-alone format
	 * @return array month names indexed by month values (1-12)
	 */
	public function getMonthNames($width = 'wide', $standAlone = false)
	{
		if ($standAlone)
			return isset($this -> _localeData['monthNamesSA'][$width]) ? $this -> _localeData['monthNamesSA'][$width] : $this -> _localeData['monthNames'][$width];
		else
			return isset($this -> _localeData['monthNames'][$width]) ? $this -> _localeData['monthNames'][$width] : $this -> _localeData['monthNamesSA'][$width];
	}

	/**
	 * @param integer $day weekday (0-6, 0 means Sunday)
	 * @param string $width weekday name width.  It can be 'wide', 'abbreviated' or
	 * 'narrow'.
	 * @param boolean $standAlone whether the week day name should be returned in
	 * stand-alone format
	 * @return string the weekday name
	 */
	public function getWeekDayName($day, $width = 'wide', $standAlone = false)
	{
		if ($standAlone)
		{
			return isset($this -> _localeData['weekDayNamesSA'][$width][$day]) ? $this -> _localeData['weekDayNamesSA'][$width][$day] : $this -> _localeData['weekDayNames'][$width][$day];
		}

		return isset($this -> _localeData['weekDayNames'][$width][$day]) ? $this -> _localeData['weekDayNames'][$width][$day] : $this -> _localeData['weekDayNamesSA'][$width][$day];
	}

	/**
	 * Returns the week day names in the specified width.
	 * @param string $width weekday name width.  It can be 'wide', 'abbreviated' or
	 * 'narrow'.
	 * @param boolean $standAlone whether the week day name should be returned in
	 * stand-alone format
	 * @return array the weekday names indexed by weekday values (0-6, 0 means
	 * Sunday, 1 Monday, etc.)
	 */
	public function getWeekDayNames($width = 'wide', $standAlone = false)
	{
		if ($standAlone)
			return isset($this -> _localeData['weekDayNamesSA'][$width]) ? $this -> _localeData['weekDayNamesSA'][$width] : $this -> _localeData['weekDayNames'][$width];
		else
			return isset($this -> _localeData['weekDayNames'][$width]) ? $this -> _localeData['weekDayNames'][$width] : $this -> _localeData['weekDayNamesSA'][$width];
	}

	/**
	 * @param integer $era era (0,1)
	 * @param string $width era name width.  It can be 'wide', 'abbreviated' or
	 * 'narrow'.
	 * @return string the era name
	 */
	public function getEraName($era, $width = 'wide')
	{
		return $this -> _localeData['eraNames'][$width][$era];
	}

	/**
	 * @return string the AM name
	 */
	public function getAMName()
	{
		return $this -> _localeData['amName'];
	}

	/**
	 * @return string the PM name
	 */
	public function getPMName()
	{
		return $this -> _localeData['pmName'];
	}

	/**
	 * @param string $width date format width. It can be 'full', 'long', 'medium' or
	 * 'short'.
	 * @return string date format
	 */
	public function getDateFormat($width = 'medium')
	{

		return $this -> _localeData['dateFormats'][$width];
	}

	/**
	 * @param string $width time format width. It can be 'full', 'long', 'medium' or
	 * 'short'.
	 * @return string date format
	 */
	public function getTimeFormat($width = 'medium')
	{
		return $this -> _localeData['timeFormats'][$width];
	}

	/**
	 * @return string datetime format, i.e., the order of date and time.
	 */
	public function getDateTimeFormat()
	{
		return $this -> _localeData['dateTimeFormat'];
	}

	/**
	 * @return string the character orientation, which is either 'ltr'
	 * (left-to-right) or 'rtl' (right-to-left)
	 * @since 1.1.2
	 */
	public function getOrientation()
	{
		return isset($this -> _localeData['orientation']) ? $this -> _localeData['orientation'] : 'ltr';
	}

	/**
	 * @return array plural forms expressions
	 */
	public function getPluralRules()
	{
		return isset($this -> _localeData['pluralRules']) ? $this -> _localeData['pluralRules'] : array();
	}

	/**
	 * Converts a locale ID to a language ID.
	 * A language ID consists of only the first group of letters before an underscore
	 * or dash.
	 * @param string $id the locale ID to be converted
	 * @return string the language ID
	 * @since 1.1.9
	 */
	public function getLanguageID($id)
	{
		// normalize id
		$id = $this -> getCanonicalID($id);
		// remove sub tags
		if (($underscorePosition = strpos($id, '_')) !== false)
		{
			$id = substr($id, 0, $underscorePosition);
		}
		return $id;
	}

	/**
	 * Converts a locale ID to a script ID.
	 * A script ID consists of only the last four characters after an underscore or
	 * dash.
	 * @param string $id the locale ID to be converted
	 * @return string the script ID
	 * @since 1.1.9
	 */
	public function getScriptID($id)
	{
		// normalize id
		$id = $this -> getCanonicalID($id);
		// find sub tags
		if (($underscorePosition = strpos($id, '_')) !== false)
		{
			$subTag = explode('_', $id);
			// script sub tags can be distigused from territory sub tags by length
			if (strlen($subTag[1]) === 4)
			{
				$id = $subTag[1];
			}
			else
			{
				$id = null;
			}
		}
		else
		{
			$id = null;
		}
		return $id;
	}

	/**
	 * Converts a locale ID to a territory ID.
	 * A territory ID consists of only the last two to three letter or digits after
	 * an underscore or dash.
	 * @param string $id the locale ID to be converted
	 * @return string the territory ID
	 * @since 1.1.9
	 */
	public function getTerritoryID($id)
	{
		// normalize id
		$id = $this -> getCanonicalID($id);
		// find sub tags
		if (($underscorePosition = strpos($id, '_')) !== false)
		{
			$subTag = explode('_', $id);
			// territory sub tags can be distigused from script sub tags by length
			if (strlen($subTag[1]) < 4)
			{
				$id = $subTag[1];
			}
			else
			{
				$id = null;
			}
		}
		else
		{
			$id = null;
		}
		return $id;
	}

	/**
	 * Gets a localized name from intl data file (one of framework/intl/data/ files).
	 *
	 * @param string $id array key from an array named by $category.
	 * @param string $category data category. One of 'languages', 'scripts' or
	 * 'territories'.
	 * @return string the localized name for the id specified. Null if data does not
	 * exist.
	 * @since 1.1.9
	 */
	public function getLocaleDisplayName($id = null, $category = 'languages')
	{
		$id = $this -> getCanonicalID($id);
		if (isset($this -> _localeData[$category][$id]))
		{
			return $this -> _localeData[$category][$id];
		}
		else
		if (($category == 'languages') && ($id = $this -> getLanguageID($id)) && (isset($this -> _localeData[$category][$id])))
		{
			return $this -> _localeData[$category][$id];
		}
		else
		if (($category == 'scripts') && ($id = $this -> getScriptID($id)) && (isset($this -> _localeData[$category][$id])))
		{
			return $this -> _localeData[$category][$id];
		}
		else
		if (($category == 'territories') && ($id = $this -> getTerritoryID($id)) && (isset($this -> _localeData[$category][$id])))
		{
			return $this -> _localeData[$category][$id];
		}
		else
		{
			return null;
		}
	}

	/**
	 * @param string $id Unicode language identifier from IETF BCP 47. For example,
	 * the code "en_US" represents U.S. English and "en_GB" represents British
	 * English.
	 * @return string the local display name for the language. Null if the language
	 * code does not exist.
	 * @since 1.1.9
	 */
	public function getLanguage($id)
	{
		return $this -> getLocaleDisplayName($id, 'languages');
	}

	/**
	 * @param string $id Unicode script identifier from IETF BCP 47. For example, the
	 * code "en_US" represents U.S. English and "en_GB" represents British English.
	 * @return string the local display name for the script. Null if the script code
	 * does not exist.
	 * @since 1.1.9
	 */
	public function getScript($id)
	{
		return $this -> getLocaleDisplayName($id, 'scripts');
	}

	/**
	 * @param string $id Unicode territory identifier from IETF BCP 47. For example,
	 * the code "en_US" represents U.S. English and "en_GB" represents British
	 * English.
	 * @return string the local display name for the territory. Null if the territory
	 * code does not exist.
	 * @since 1.1.9
	 */
	public function getTerritory($id)
	{
		return $this -> getLocaleDisplayName($id, 'territories');
	}

	/**
	 * Formats a date according to a customized pattern.
	 * @param string $pattern the pattern (See {@link
	 * http://www.unicode.org/reports/tr35/#Date_Format_Patterns})
	 * @param mixed $time UNIX timestamp or a string in strtotime format
	 * @return string formatted date time.
	 */
	protected function formatDate($pattern, $time)
	{
		if (is_string($time))
		{
			if (ctype_digit($time))
				$time = (int)$time;
			else
				$time = strtotime($time);
		}
		$date = Intl_Timestamp::getDate($time, false, false);
		$tokens = $this -> parseDateFormat($pattern);
		foreach ($tokens as &$token)
		{
			if (is_array($token))// a callback: method name, sub-pattern
				$token = $this -> {$token[0]}($token[1], $date);
		}
		return implode('', $tokens);
	}

	/**
	 * Formats a date according to a predefined pattern.
	 * The predefined pattern is determined based on the date pattern width and time
	 * pattern width.
	 * @param mixed $timestamp UNIX timestamp or a string in strtotime format
	 * @param string $dateWidth width of the date pattern. It can be 'full', 'long',
	 * 'medium' and 'short'.
	 * If null, it means the date portion will NOT appear in the formatting result
	 * @param string $timeWidth width of the time pattern. It can be 'full', 'long',
	 * 'medium' and 'short'.
	 * If null, it means the time portion will NOT appear in the formatting result
	 * @return string formatted date time.
	 */
	public function formatDateTime($timestamp, $dateWidth = 'medium', $timeWidth = 'medium')
	{
		if (!empty($dateWidth))
		{

			$date = $this -> formatDate($this -> getDateFormat($dateWidth), $timestamp);
		}

		if (!empty($timeWidth))
		{

			$time = $this -> formatDate($this -> getTimeFormat($timeWidth), $timestamp);
		}

		if (isset($date) && isset($time))
		{
			$dateTimePattern = $this -> getDateTimeFormat();

			return strtr($dateTimePattern, array(
				'{0}' => $time,
				'{1}' => $date
			));
		}
		else
		if (isset($date))
		{

			return $date;
		}
		else
		if (isset($time))
		{
			return $time;
		}
	}

	/**
	 * Parses the datetime format pattern.
	 * @param string $pattern the pattern to be parsed
	 * @return array tokenized parsing result
	 */
	protected function parseDateFormat($pattern)
	{
		static $formats = array();
		// cache
		if (isset($formats[$pattern]))
			return $formats[$pattern];
		$tokens = array();
		$n = strlen($pattern);
		$isLiteral = false;
		$literal = '';
		for ($i = 0; $i < $n; ++$i)
		{
			$c = $pattern[$i];
			if ($c === "'")
			{
				if ($i < $n - 1 && $pattern[$i + 1] === "'")
				{
					$tokens[] = "'";
					$i++;
				}
				else
				if ($isLiteral)
				{
					$tokens[] = $literal;
					$literal = '';
					$isLiteral = false;
				}
				else
				{
					$isLiteral = true;
					$literal = '';
				}
			}
			else
			if ($isLiteral)
				$literal .= $c;
			else
			{
				for ($j = $i + 1; $j < $n; ++$j)
				{
					if ($pattern[$j] !== $c)
						break;
				}
				$p = str_repeat($c, $j - $i);
				if (isset(self::$_dateFormats[$c]))
					$tokens[] = array(
						self::$_dateFormats[$c],
						$p
					);
				else
					$tokens[] = $p;
				$i = $j - 1;
			}
		}
		if ($literal !== '')
			$tokens[] = $literal;

		return $formats[$pattern] = $tokens;
	}

	/**
	 * Get the year.
	 * "yy" will return the last two digits of year.
	 * "y...y" will pad the year with 0 in the front, e.g. "yyyyy" will generate
	 * "02008" for year 2008.
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return string formatted year
	 */
	protected function formatYear($pattern, $date)
	{
		$year = $date['year'];
		if ($pattern === 'yy')
			return str_pad($year % 100, 2, '0', STR_PAD_LEFT);
		else
			return str_pad($year, strlen($pattern), '0', STR_PAD_LEFT);
	}

	/**
	 * Get the month.
	 * "M" will return integer 1 through 12;
	 * "MM" will return two digits month number with necessary zero padding, e.g. 05;
	 * "MMM" will return the abrreviated month name, e.g. "Jan";
	 * "MMMM" will return the full month name, e.g. "January";
	 * "MMMMM" will return the narrow month name, e.g. "J";
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return string month name
	 */
	protected function formatMonth($pattern, $date)
	{
		$month = $date['mon'];
		switch($pattern)
		{
			case 'M' :
				return $month;
			case 'MM' :
				return str_pad($month, 2, '0', STR_PAD_LEFT);
			case 'MMM' :
				return $this -> getMonthName($month, 'abbreviated');
			case 'MMMM' :
				return $this -> getMonthName($month, 'wide');
			case 'MMMMM' :
				return $this -> getMonthName($month, 'narrow');
			case 'L' :
				return $month;
			case 'LL' :
				return str_pad($month, 2, '0', STR_PAD_LEFT);
			case 'LLL' :
				return $this -> getMonthName($month, 'abbreviated', true);
			case 'LLLL' :
				return $this -> getMonthName($month, 'wide', true);
			case 'LLLLL' :
				return $this -> getMonthName($month, 'narrow', true);
			default :
				throw new Exception(strtr('The pattern for month must be "M", "MM", "MMM", "MMMM", "L", "LL", "LLL" or "LLLL".'));
		}
	}

	/**
	 * Get the day of the month.
	 * "d" for non-padding, "dd" will always return 2 digits day numbers, e.g. 05.
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return string day of the month
	 */
	protected function formatDay($pattern, $date)
	{
		$day = $date['mday'];
		if ($pattern === 'd')
			return $day;
		else
		if ($pattern === 'dd')
			return str_pad($day, 2, '0', STR_PAD_LEFT);
		else
			throw new Exception('The pattern for day of the month must be "d" or "dd".');
	}

	/**
	 * Get the day in the year, e.g. [1-366]
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return integer hours in AM/PM format.
	 */
	protected function formatDayInYear($pattern, $date)
	{
		$day = $date['yday'];
		if (($n = strlen($pattern)) <= 3)
			return str_pad($day, $n, '0', STR_PAD_LEFT);
		else
			throw new Exception('The pattern for day in year must be "D", "DD" or "DDD".');
	}

	/**
	 * Get day of week in the month, e.g. 2nd Wed in July.
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return integer day in month
	 * @see http://www.unicode.org/reports/tr35/#Date_Format_Patterns
	 */
	protected function formatDayInMonth($pattern, $date)
	{
		if ($pattern === 'F')
		{
			return (int)(($date['mday'] + 6) / 7);
		}
		else
		{
			throw new Exception('The pattern for day in month must be "F".');
		}
	}

	/**
	 * Get the day of the week.
	 * "E", "EE", "EEE" will return abbreviated week day name, e.g. "Tues";
	 * "EEEE" will return full week day name;
	 * "EEEEE" will return the narrow week day name, e.g. "T";
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return string day of the week.
	 * @see http://www.unicode.org/reports/tr35/#Date_Format_Patterns
	 */
	protected function formatDayInWeek($pattern, $date)
	{
		$day = $date['wday'];
		switch($pattern)
		{
			case 'E' :
			case 'EE' :
			case 'EEE' :
			case 'eee' :
				return $this -> getWeekDayName($day, 'abbreviated');
			case 'EEEE' :
			case 'eeee' :
				return $this -> getWeekDayName($day, 'wide');
			case 'EEEEE' :
			case 'eeeee' :
				return $this -> getWeekDayName($day, 'narrow');
			case 'e' :
			case 'ee' :
			case 'c' :
				return $day ? $day : 7;
			case 'ccc' :
				return $this -> getWeekDayName($day, 'abbreviated', true);
			case 'cccc' :
				return $this -> getWeekDayName($day, 'wide', true);
			case 'ccccc' :
				return $this -> getWeekDayName($day, 'narrow', true);
			default :
				throw new Exception('The pattern for day of the week must be "E", "EE", "EEE", "EEEE", "EEEEE", "e", "ee", "eee", "eeee", "eeeee", "c", "cccc" or "ccccc".');
		}
	}

	/**
	 * Get the AM/PM designator, 12 noon is PM, 12 midnight is AM.
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return string AM or PM designator
	 */
	protected function formatPeriod($pattern, $date)
	{
		if ($pattern === 'a')
		{
			if (intval($date['hours'] / 12))
				return $this -> getPMName();
			else
				return $this -> getAMName();
		}
		else
			throw new Exception('The pattern for AM/PM marker must be "a".');
	}

	/**
	 * Get the hours in 24 hour format, i.e. [0-23].
	 * "H" for non-padding, "HH" will always return 2 characters.
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return string hours in 24 hour format.
	 */
	protected function formatHour24($pattern, $date)
	{
		$hour = $date['hours'];
		if ($pattern === 'H')
			return $hour;
		else
		if ($pattern === 'HH')
			return str_pad($hour, 2, '0', STR_PAD_LEFT);
		else
			throw new Exception('The pattern for 24 hour format must be "H" or "HH".');
	}

	/**
	 * Get the hours in 12 hour format, i.e., [1-12]
	 * "h" for non-padding, "hh" will always return 2 characters.
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return string hours in 12 hour format.
	 */
	protected function formatHour12($pattern, $date)
	{
		$hour = $date['hours'];
		$hour = ($hour == 12 | $hour == 0) ? 12 : ($hour) % 12;
		if ($pattern === 'h')
			return $hour;
		else
		if ($pattern === 'hh')
			return str_pad($hour, 2, '0', STR_PAD_LEFT);
		else
			throw new Exception('The pattern for 12 hour format must be "h" or "hh".');
	}

	/**
	 * Get the hours [1-24].
	 * 'k' for non-padding, and 'kk' with 2 characters padding.
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return integer hours [1-24]
	 */
	protected function formatHourInDay($pattern, $date)
	{
		$hour = $date['hours'] == 0 ? 24 : $date['hours'];
		if ($pattern === 'k')
			return $hour;
		else
		if ($pattern === 'kk')
			return str_pad($hour, 2, '0', STR_PAD_LEFT);
		else
			throw new Exception('The pattern for hour in day must be "k" or "kk".');
	}

	/**
	 * Get the hours in AM/PM format, e.g [0-11]
	 * "K" for non-padding, "KK" will always return 2 characters.
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return integer hours in AM/PM format.
	 */
	protected function formatHourInPeriod($pattern, $date)
	{
		$hour = $date['hours'] % 12;
		if ($pattern === 'K')
			return $hour;
		else
		if ($pattern === 'KK')
			return str_pad($hour, 2, '0', STR_PAD_LEFT);
		else
			throw new Exception('The pattern for hour in AM/PM must be "K" or "KK".');
	}

	/**
	 * Get the minutes.
	 * "m" for non-padding, "mm" will always return 2 characters.
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return string minutes.
	 */
	protected function formatMinutes($pattern, $date)
	{
		$minutes = $date['minutes'];
		if ($pattern === 'm')
			return $minutes;
		else
		if ($pattern === 'mm')
			return str_pad($minutes, 2, '0', STR_PAD_LEFT);
		else
			throw new Exception('The pattern for minutes must be "m" or "mm".');
	}

	/**
	 * Get the seconds.
	 * "s" for non-padding, "ss" will always return 2 characters.
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return string seconds
	 */
	protected function formatSeconds($pattern, $date)
	{
		$seconds = $date['seconds'];
		if ($pattern === 's')
			return $seconds;
		else
		if ($pattern === 'ss')
			return str_pad($seconds, 2, '0', STR_PAD_LEFT);
		else
			throw new Exception('The pattern for seconds must be "s" or "ss".');
	}

	/**
	 * Get the week in the year.
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return integer week in year
	 */
	protected function formatWeekInYear($pattern, $date)
	{
		if ($pattern === 'w')
			return @date('W', @mktime(0, 0, 0, $date['mon'], $date['mday'], $date['year']));
		else
			throw new Exception('The pattern for week in year must be "w".');
	}

	/**
	 * Get week in the month.
	 * @param array $pattern result of {@link CTimestamp::getdate}.
	 * @param string $date a pattern.
	 * @return integer week in month
	 */
	protected function formatWeekInMonth($pattern, $date)
	{
		if ($pattern === 'W')
			return @date('W', @mktime(0, 0, 0, $date['mon'], $date['mday'], $date['year'])) - date('W', mktime(0, 0, 0, $date['mon'], 1, $date['year'])) + 1;
		else
			throw new Exception('The pattern for week in month must be "W".');
	}

	/**
	 * Get the timezone of the server machine.
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return string time zone
	 * @todo How to get the timezone for a different region?
	 */
	protected function formatTimeZone($pattern, $date)
	{
		if ($pattern[0] === 'z' || $pattern[0] === 'v')
			return @date('T', @mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']));
		elseif ($pattern[0] === 'Z')
			return @date('O', @mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']));
		else
			throw new Exception('The pattern for time zone must be "z" or "v".');
	}

	/**
	 * Get the era. i.e. in gregorian, year > 0 is AD, else BC.
	 * @param string $pattern a pattern.
	 * @param array $date result of {@link CTimestamp::getdate}.
	 * @return string era
	 * @todo How to support multiple Eras?, e.g. Japanese.
	 */
	protected function formatEra($pattern, $date)
	{
		$era = $date['year'] > 0 ? 1 : 0;
		switch($pattern)
		{
			case 'G' :
			case 'GG' :
			case 'GGG' :
				return $this -> getEraName($era, 'abbreviated');
			case 'GGGG' :
				return $this -> getEraName($era, 'wide');
			case 'GGGGG' :
				return $this -> getEraName($era, 'narrow');
			default :
				throw new Exception('The pattern for era must be "G", "GG", "GGG", "GGGG" or "GGGGG".');
		}
	}

	/**
	 * Formats a number using the currency format defined in the locale.
	 * @param mixed $value the number to be formatted
	 * @param string $currency 3-letter ISO 4217 code. For example, the code "USD" represents the US Dollar and "EUR" represents the Euro currency.
	 * The currency placeholder in the pattern will be replaced with the currency symbol.
	 * @return string the formatting result.
	 */
	public function formatCurrency($value, $currency)
	{
		$pattern = $this -> getCurrencyFormat();

		$format = $this -> parseNumberFormat($pattern);

		$result = $this -> formatNumber($format, $value);

		if (($symbol = $this -> getCurrencySymbol($currency)) === null)
		{
			$symbol = $currency;
		}
		return str_replace('¤', $symbol, $result);
	}

	/**
	 * Formats a number using the percentage format defined in the locale.
	 * Note, if the percentage format contains '%', the number will be multiplied by 100 first.
	 * If the percentage format contains '‰', the number will be multiplied by 1000.
	 * @param mixed $value the number to be formatted
	 * @return string the formatting result.
	 */
	public function formatPercentage($value)
	{
		$pattern = $this -> getPercentFormat();
		$format = $this -> parseNumberFormat($pattern);
		return $this -> formatNumber($format, $value);
	}

	/**
	 * Formats a number using the decimal format defined in the locale.
	 * @param mixed $value the number to be formatted
	 * @return string the formatting result.
	 */
	public function formatDecimal($value)
	{
		$pattern = $this -> getDecimalFormat();
		$format = $this -> parseNumberFormat($pattern);
		return $this -> formatNumber($format, $value);
	}

	/**
	 * Formats a number based on a format.
	 * This is the method that does actual number formatting.
	 * @param array $format format with the following structure:
	 * <pre>
	 * array(
	 * 	'decimalDigits'=>2,     // number of required digits after decimal point; 0s will be padded if not enough digits; if -1, it means we should drop decimal point
	 *  'maxDecimalDigits'=>3,  // maximum number of digits after decimal point. Additional digits will be truncated.
	 * 	'integerDigits'=>1,     // number of required digits before decimal point; 0s will be padded if not enough digits
	 * 	'groupSize1'=>3,        // the primary grouping size; if 0, it means no grouping
	 * 	'groupSize2'=>0,        // the secondary grouping size; if 0, it means no secondary grouping
	 * 	'positivePrefix'=>'+',  // prefix to positive number
	 * 	'positiveSuffix'=>'',   // suffix to positive number
	 * 	'negativePrefix'=>'(',  // prefix to negative number
	 * 	'negativeSuffix'=>')',  // suffix to negative number
	 * 	'multiplier'=>1,        // 100 for percent, 1000 for per mille
	 * );
	 * </pre>
	 * @param mixed $value the number to be formatted
	 * @return string the formatted result
	 */
	protected function formatNumber($format, $value)
	{
		$negative = $value < 0;
		$value = abs($value * $format['multiplier']);
		if ($format['maxDecimalDigits'] >= 0)
			$value = round($value, $format['maxDecimalDigits']);
		$value = "$value";
		if (($pos = strpos($value, '.')) !== false)
		{
			$integer = substr($value, 0, $pos);
			$decimal = substr($value, $pos + 1);
		}
		else
		{
			$integer = $value;
			$decimal = '';
		}

		if ($format['decimalDigits'] > strlen($decimal))
			$decimal = str_pad($decimal, $format['decimalDigits'], '0');
		if (strlen($decimal) > 0)
			$decimal = $this -> getNumberSymbol('decimal') . $decimal;

		$integer = str_pad($integer, $format['integerDigits'], '0', STR_PAD_LEFT);
		if ($format['groupSize1'] > 0 && strlen($integer) > $format['groupSize1'])
		{
			$str1 = substr($integer, 0, -$format['groupSize1']);
			$str2 = substr($integer, -$format['groupSize1']);
			$size = $format['groupSize2'] > 0 ? $format['groupSize2'] : $format['groupSize1'];
			$str1 = str_pad($str1, (int)((strlen($str1) + $size - 1) / $size) * $size, ' ', STR_PAD_LEFT);
			$integer = ltrim(implode($this -> getNumberSymbol('group'), str_split($str1, $size))) . $this -> getNumberSymbol('group') . $str2;
		}

		if ($negative)
			$number = $format['negativePrefix'] . $integer . $decimal . $format['negativeSuffix'];
		else
			$number = $format['positivePrefix'] . $integer . $decimal . $format['positiveSuffix'];

		return strtr($number, array(
			'%' => $this -> getNumberSymbol('percentSign'),
			'‰' => $this -> getNumberSymbol('perMille')
		));
	}

	/**
	 * Parses a given string pattern.
	 * @param string $pattern the pattern to be parsed
	 * @return array the parsed pattern
	 * @see formatNumber
	 */
	protected function parseNumberFormat($pattern)
	{
		if (isset($this -> _numberFormats[$pattern]))
			return $this -> _numberFormats[$pattern];

		$format = array();

		// find out prefix and suffix for positive and negative patterns
		$patterns = explode(';', $pattern);
		$format['positivePrefix'] = $format['positiveSuffix'] = $format['negativePrefix'] = $format['negativeSuffix'] = '';
		if (preg_match('/^(.*?)[#,\.0]+(.*?)$/', $patterns[0], $matches))
		{
			$format['positivePrefix'] = $matches[1];
			$format['positiveSuffix'] = $matches[2];
		}

		if (isset($patterns[1]) && preg_match('/^(.*?)[#,\.0]+(.*?)$/', $patterns[1], $matches))// with a negative pattern
		{
			$format['negativePrefix'] = $matches[1];
			$format['negativeSuffix'] = $matches[2];
		}
		else
		{
			$format['negativePrefix'] = $this -> getNumberSymbol('minusSign') . $format['positivePrefix'];
			$format['negativeSuffix'] = $format['positiveSuffix'];
		}
		$pat = $patterns[0];

		// find out multiplier
		if (strpos($pat, '%') !== false)
			$format['multiplier'] = 100;
		else
		if (strpos($pat, '‰') !== false)
			$format['multiplier'] = 1000;
		else
			$format['multiplier'] = 1;

		// find out things about decimal part
		if (($pos = strpos($pat, '.')) !== false)
		{
			if (($pos2 = strrpos($pat, '0')) > $pos)
				$format['decimalDigits'] = $pos2 - $pos;
			else
				$format['decimalDigits'] = 0;
			if (($pos3 = strrpos($pat, '#')) >= $pos2)
				$format['maxDecimalDigits'] = $pos3 - $pos;
			else
				$format['maxDecimalDigits'] = $format['decimalDigits'];
			$pat = substr($pat, 0, $pos);
		}
		else// no decimal part
		{
			$format['decimalDigits'] = 0;
			$format['maxDecimalDigits'] = 0;
		}

		// find out things about integer part
		$p = str_replace(',', '', $pat);
		if (($pos = strpos($p, '0')) !== false)
			$format['integerDigits'] = strrpos($p, '0') - $pos + 1;
		else
			$format['integerDigits'] = 0;
		// find out group sizes. some patterns may have two different group sizes
		$p = str_replace('#', '0', $pat);
		if (($pos = strrpos($pat, ',')) !== false)
		{
			$format['groupSize1'] = strrpos($p, '0') - $pos;
			if (($pos2 = strrpos(substr($p, 0, $pos), ',')) !== false)
				$format['groupSize2'] = $pos - $pos2 - 1;
			else
				$format['groupSize2'] = 0;
		}
		else
			$format['groupSize1'] = $format['groupSize2'] = 0;

		return $this -> _numberFormats[$pattern] = $format;
	}

}
